﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;

namespace Graphics3
{
    class Line
    {
        public Point StartPoint { get; set; }
        public Point EndPoint { get; set; }
        public int Thickness { get; set; }
        public Color Color { get; set; }
        public bool Antialiasing { get; set; }

        public Point MiddlePoint
        {
            get
            {
                var middlePoint = new Point(Math.Abs(StartPoint.X - EndPoint.X) / 2, Math.Abs(StartPoint.Y - EndPoint.Y) / 2);

                middlePoint.X += StartPoint.X < EndPoint.X ? StartPoint.X : EndPoint.X;
                middlePoint.Y += StartPoint.Y < EndPoint.Y ? StartPoint.Y : EndPoint.Y;

                return middlePoint;
            }
        }

        public void Draw(Graphics graphics)
        {
            var pen = new Pen(this.Color);
            pen.Width = this.Thickness;
            //graphics.DrawEllipse(pen, MiddlePoint.X, MiddlePoint.Y, 30, 30);
            graphics.DrawLine(pen, StartPoint, EndPoint);
        }

        public Bitmap Draw(Bitmap bitmap, Color backgroundColor)
        {
            if (this.Antialiasing)
            {
                return DrawWtihAntiAliasing(this.StartPoint, this.EndPoint, bitmap, backgroundColor, this.Color, this.Thickness);
            }
            else
            {
                var points = BresenhamThickLine(this.StartPoint, this.EndPoint, this.Thickness);
                return Draw(points, bitmap);
            }
        }

        private Bitmap Draw(IEnumerable<Point> points, Bitmap bitmap)
        {
            var result = new Bitmap(bitmap);

            BitmapData resultData = result.LockBits(
                new Rectangle(0, 0, result.Width, result.Height),
                ImageLockMode.ReadWrite,
                result.PixelFormat);

            var groupedPoints = points.GroupBy(point => point.Y).OrderBy(group => group.Key);

            unsafe
            {
                foreach (var group in groupedPoints)
                { 
                    var y = group.Key;
                //for (int y = 0; y < resultData.Height; y++)
                //{ 
                    if (y >= 0 && y < result.Height)
                    {
                        byte* row = (byte*)resultData.Scan0 + (y * resultData.Stride);
                        var orderedPoints = group.OrderBy(point => point.X);
                        foreach (var point in orderedPoints)
                        {
                            var x = point.X;
                            //for (int x = 0; x < resultData.Width; x++)
                            //{
                            if (x >= 0 && x < result.Width)
                            {
                                row[x * 4] = this.Color.B;
                                row[(x * 4) + 1] = this.Color.G;
                                row[(x * 4) + 2] = this.Color.R;
                                row[(x * 4) + 3] = this.Color.A;
                            }
                        }
                    }
                }
            }

            result.UnlockBits(resultData);

            return result;
        }

        public static Line PreviewLine(Point startPoint, Point endPoint)
        {
            var previewLine = new Line()
            {
                StartPoint = startPoint,
                EndPoint = endPoint,
                Thickness = 1,
                Color = Color.Black,
                Antialiasing = false
            };

            return previewLine;
        }

        public int GetPointsDistanceFrom(Point point)
        {
            var result = point.Distance(StartPoint);
            var temp = point.Distance(MiddlePoint);
            if (temp < result)
                result = temp;
            temp = point.Distance(EndPoint);
            if (temp < result)
                result = temp;

            return result;
        }

        public int GetLineDistanceFrom(Point point)
        {
            var distance = Math.Abs(((EndPoint.X - StartPoint.X) * (StartPoint.Y - point.Y)) - ((StartPoint.X - point.X) * (EndPoint.Y - StartPoint.Y)) )
                / Math.Sqrt(Math.Pow(EndPoint.X - StartPoint.X, 2) + Math.Pow(EndPoint.Y - StartPoint.Y, 2));

            return (int)Math.Round(distance);
        }

        public int GetUniversalDistance(Point point)
        {
            return GetPointsDistanceFrom(point) + GetLineDistanceFrom(point);
        }

        public Bitmap DrawAsSelected(Graphics graphics, Bitmap bitmap)
        {
            var pen = new Pen(Color.Red);
            var middlePoint = this.MiddlePoint;
            graphics.DrawEllipse(pen, this.StartPoint.X - 20, this.StartPoint.Y - 20, 40, 40);
            graphics.DrawEllipse(pen, this.EndPoint.X - 20, this.EndPoint.Y - 20, 40, 40);
            graphics.DrawEllipse(pen, middlePoint.X - 20, middlePoint.Y - 20, 40, 40);

            var points = BresenhamThickLine(this.StartPoint, this.EndPoint, this.Thickness);
            return Draw(points, bitmap);
        }

        private static Bitmap BresenhamLine(Point startPoint, Point endPoint, Bitmap bitmap, Color color)
        {
            var resultBitmap = new Bitmap(bitmap);

            bool steep = Math.Abs(endPoint.Y - startPoint.Y) > Math.Abs(endPoint.X - startPoint.X);
            if (steep)
            {
                startPoint = new Point(startPoint.Y, startPoint.X);
                endPoint = new Point(endPoint.Y, endPoint.X);
            }
            
            if (startPoint.X > endPoint.X) 
            {
                var temp = startPoint;
                startPoint = endPoint;
                endPoint = temp;
            }
            
            int dX = (endPoint.X - startPoint.X);
            int dY = Math.Abs(endPoint.Y - startPoint.Y);
            int err = (dX / 2);
            int ystep = (startPoint.Y < endPoint.Y ? 1 : -1);
            int y = startPoint.Y;

            for (int x = startPoint.X; x <= endPoint.X; ++x)
            {
                if (steep)
                    resultBitmap.SetPixel(y, x, color);
                else
                    resultBitmap.SetPixel(x, y, color);
                err = err - dY;
                if (err < 0) 
                { 
                    y += ystep;
                    err += dX; 
                }
            }

            return resultBitmap;
        }

        private static IEnumerable<Point> BresenhamThickLine(Point startPoint, Point endPoint, int thickness)
        {
            var thikness1 = (int)Math.Ceiling(thickness / 2d);
            var thikness2 = (int)Math.Floor(thickness / 2d);
            var result = new List<Point>();

            bool steep = Math.Abs(endPoint.Y - startPoint.Y) > Math.Abs(endPoint.X - startPoint.X);
            if (steep)
            {
                startPoint = new Point(startPoint.Y, startPoint.X);
                endPoint = new Point(endPoint.Y, endPoint.X);
            }

            if (startPoint.X > endPoint.X)
            {
                var temp = startPoint;
                startPoint = endPoint;
                endPoint = temp;
            }

            int dX = (endPoint.X - startPoint.X);
            int dY = Math.Abs(endPoint.Y - startPoint.Y);
            int err = (dX / 2);
            int ystep = (startPoint.Y < endPoint.Y ? 1 : -1);
            int y = startPoint.Y;

            for (int x = startPoint.X; x <= endPoint.X; ++x)
            {
                int xToSet;
                int yToSet;

                if (steep)
                {
                    xToSet = y;
                    yToSet = x;
                    for (int i = xToSet - thikness1; i < xToSet + thikness2; ++i)
                    {
                        result.Add(new Point(i, yToSet));
                    }
                }
                else
                {
                    xToSet = x;
                    yToSet = y;
                    for (int i = yToSet - thikness1; i < yToSet + thikness2; ++i)
                    {
                        result.Add(new Point(xToSet, i));
                    }
                }
                err = err - dY;
                if (err < 0)
                {
                    y += ystep;
                    err += dX;
                }
            }

            return result;
        }

        private static void SetPixelOnBitmap(int x, int y, Color color, Bitmap bitmap)
        {
            if (x < bitmap.Width && y < bitmap.Height)
            {
                bitmap.SetPixel(x, y, color);
            }
        }

        private static Bitmap DrawWtihAntiAliasing(Point startPoint, Point endPoint, Bitmap bitmap, Color backgroundColor, Color color, int thickness)
        {
            var bitmapWidth = Math.Abs(endPoint.X - startPoint.X);
            var bitmapHeight = Math.Abs(endPoint.Y - startPoint.Y);

            bool steep = Math.Abs(endPoint.Y - startPoint.Y) > Math.Abs(endPoint.X - startPoint.X);
            var thikness1 = (int)Math.Ceiling(thickness / 2d);
            var thikness2 = (int)Math.Floor(thickness / 2d);

            if (steep)
                bitmapWidth += thickness;
            else
                bitmapHeight += thickness;

            int newStartX, newStartY, newEndX, newEndY;
            int mountX = 0;
            int mountY = 0;

            if (startPoint.X < endPoint.X)
            {
                newStartX = 0;
                newEndX = bitmapWidth;
                mountX = startPoint.X;
                if (steep)
                {
                    newStartX += thikness1;
                    newEndX -= thikness2;
                    mountX -= thikness1;
                }
            }
            else
            {
                newStartX = bitmapWidth;
                newEndX = 0;
                mountX = endPoint.X;
                if (steep)
                {
                    newStartX -= thikness2;
                    newEndX += thikness1;
                    mountX -= thikness1;
                }
            }

            if (startPoint.Y < endPoint.Y)
            {
                newStartY = 0;
                newEndY = bitmapHeight;
                mountY = startPoint.Y;
                if (!steep)
                {
                    newStartY += thikness1;
                    newEndY -= thikness2;
                    mountY -= thikness1;
                }
            }
            else
            {
                newStartY = bitmapHeight;
                newEndY = 0;
                mountY = endPoint.Y;
                if (!steep)
                {
                    newStartY -= thikness2;
                    newEndY += thikness1;
                    mountY -= thikness1;
                }
            }

            var points = BresenhamThickLine(
                new Point(newStartX * 2, newStartY * 2),
                new Point(newEndX * 2, newEndY * 2),
                thickness * 2);

            return MergeBitmaps(points, bitmap, backgroundColor, new Point(mountX, mountY), color, thickness);            
        }

        private static Bitmap MergeBitmaps(IEnumerable<Point> precisePoints, Bitmap bitmap, Color bitmapBackColor, Point point, Color color, int thickness)
        {
            var result = new Bitmap(bitmap);

            BitmapData resultData = result.LockBits(
                new Rectangle(0, 0, result.Width, result.Height),
                ImageLockMode.ReadWrite,
                result.PixelFormat);

            var height = precisePoints.Max(p => p.Y);
            var width = precisePoints.Max(p => p.X);

            bool[,] precisePointsMap = new bool[width + 2, height + 2];
            foreach (var p in precisePoints)
            {
                precisePointsMap[p.X, p.Y] = true;
            }

            unsafe
            {
                for (int preciseY = 0; preciseY <= height; preciseY += 2)
                {
                    var y = point.Y + (preciseY / 2);
                    byte* row = (byte*)resultData.Scan0 + (y * resultData.Stride);

                    if (y >= 0 && y < result.Height)
                    {
                        for (int preciseX = 0; preciseX <= width; preciseX += 2)
                        {
                            var x = point.X + (preciseX / 2);
                            if (x >= 0 && x < result.Width)
                            {
                                byte saturation = 0;
                                for (int ay = 0; ay < 2; ++ay)
                                {
                                    for (int ax = 0; ax < 2; ++ax)
                                    {
                                        if (precisePointsMap[preciseX + ax, preciseY + ay])
                                        //if (precisePoints.Contains(new Point(preciseX + ax, preciseY + ay)))
                                        {
                                            saturation++;
                                        }
                                    }
                                }

                                var alpha = (255 / 4) * saturation;
                                var backgroundAlpha = row[(x * 4) + 3];
                                byte backgroundB;
                                byte backgroundG;
                                byte backgroundR;
                                if (backgroundAlpha == 0)
                                {
                                    backgroundB = bitmapBackColor.B;
                                    backgroundG = bitmapBackColor.G;
                                    backgroundR = bitmapBackColor.R;
                                }
                                else
                                {
                                    backgroundB = row[x * 4];
                                    backgroundG = row[(x * 4) + 1];
                                    backgroundR = row[(x * 4) + 2];
                                }

                                var newColorR = (color.R * alpha / 255) + (backgroundR * (255 - alpha) / 255);
                                var newColorG = (color.G * alpha / 255) + (backgroundG * (255 - alpha) / 255);
                                var newColorB = (color.B * alpha / 255) + (backgroundB * (255 - alpha) / 255);

                                row[x * 4] = (byte)newColorB;
                                row[(x * 4) + 1] = (byte)newColorG;
                                row[(x * 4) + 2] = (byte)newColorR;
                                row[(x * 4) + 3] = 255; //alpha
                            }
                        }
                    }
                }
            }
            result.UnlockBits(resultData);

            return result;
        }
    }
}
